<?php
Junction_FileCabinet::package("Junction_Core_Handlers");
Junction_FileCabinet::package("Junction_Db_Common");
Junction_FileCabinet::package("Junction_Query");
Junction_FileCabinet::package("Junction_Utils");
Junction_FileCabinet::package("Junction_Utils_Reflection");
//Junction_FileCabinet::using("Junction_Core_Iterator");
Junction_FileCabinet::using("Junction_Utils_Config");

define("MAPPING_GET", "get");
define("MAPPING_SET", "set");

/**
 * Encapsulate data about a class and its relationship to a table
 * 
 * <p>This class stands between the client and the database.  It will 
 * generate SQL queries to perform operations on the client's object,
 * as defined in the XML schema.  Essentially this class maps the 
 * client's object and field names to the database columns.
 * 
 * @package junction.core
 */
class Junction_Core_Mapping implements IteratorAggregate {
	
	/**
	 * Name of the client's class which maps to this table
	 *
	 * @var String
	 */
	private $_classname;
		
	/**
	 * Table name for this class
	 *
	 * @var String
	 */
	private $_table;
	
	/**
	 * Pointer to the id property for this class 
	 *
	 * @var Junction_Core_Property
	 */
	private $_id;

	/**
	 * Array of properties belonging to this class
	 *
	 * @var array
	 */
	private $_properties;
	
	/**
	 * @var Junction_Db_Common_Service
	 */
	private $_dbh;
	
	/**
	 * Create a new instance of the Core_Mapping.
	 *
	 * @param String $classname
	 * @param String $table
	 */
	public function __construct($classname, $table) {
		$this->_classname = $classname;
		$this->_table = $table;
		$this->_properties = array();
		$config = Junction_Utils_Config::getSingleton();
		$this->_dbh = Junction_Db_Common_Factory::construct($config->read("dsn"));
	}
	
	/**
	 * Add a new property to the mapping.
	 *
	 * @param Core_Property $property
	 */
	public function bindProperty(Junction_Core_Property $property) {
		$this->_properties[] = $property;
	}
	
	/**
	 * Add the id to the mapping.
	 *
	 * @param Core_Property $property
	 */
	public function bindKey(Junction_Core_Property $property) {
		$this->_id = $property;
	}
	
	/**
	 * Retrieve an iterator which walks over the class properties.
	 * 
	 * @return Iterator
	 */
	public function getIterator() {
		$properties = $this->_properties;
		$properties[] = $this->_id;
		return new ArrayIterator($properties);
	}
	
	/**
	 * Save the given object's state to the database.
	 * 
	 * <p>If the object has an id then an update query will be
	 * performed, otherwise an insert query will be executed.
	 * In the event that the object is new then upon successful
	 * insert it will be assigned an id.
	 *
	 * @param Object $object
	 * @return boolean
	 */
	public function save($object) {
		if ($this->getProperty($object, $this->_id) != null) {
			return $this->update($object);
		}
		return $this->insert($object);
	}
	
	/**
	 * Insert a new row into the database.
	 * 
	 * <p>Construct a query to insert a new row into the database.
	 * The row will not include a value for the id column.  In the
	 * event that the query is successful the object's id property
	 * will be set.
	 *
	 * @param Object $object
	 * @return boolean
	 */
	private function insert($object) {
		$query = $this->buildWithObject(new Junction_Query_Insert(), $object);
		if ($this->_dbh->save($query)) {
			$this->setProperty($object, $this->_id, $this->_dbh->lastInsertId());
			return true;
		}
		return false;
	}
	
	/**
	 * Update an existing row in the database.
	 * 
	 * @param Object $object
	 * @return boolean
	 */
	private function update($object) {
		$query = $this->buildWithObject(new Junction_Query_Update(), $object);
		$query .= " WHERE " . $this->_id->getColumn() . " = " . $this->getProperty($object, $this->_id);
		return $this->_dbh->save($query);
	}
	
	/**
	 * Delete some number of rows from the database.
	 *
	 * @param String $clause the where clause for the statement
	 * @return int the number of rows deleted
	 */
	public function delete($clause) {
		$query = $this->build(new Junction_Query_Delete());
		$query .= $clause;
		$this->_dbh->save($query);
		return $this->_dbh->affectedRows();
	}
	
	/**
	 * Select some number of rows from the database.
	 * 
	 * <p>The mapping will convert the selected rows into new instances
	 * of the object.
	 * 
	 * TODO investigate whether this is a performance bottleneck
	 * for large result sets.
	 *
	 * @param String $clause the where clause for the statement
	 * @return Junction_Core_Iterator an iterator containing the client objects
	 */
	public function fetch($clause) {
		$query = $this->build(new Junction_Query_Select());
		$query .= $clause;
		return new Junction_Core_Iterator($this->_dbh->select($query), $this);
	}
	
	/**
	 * Translate a given application name into its corresponding column name.
	 * 
	 * TODO: Should we consider putting the properties in a hash such that they
	 * can be accessed in O(1) time as opposed to O(n)
	 * 
	 * @throws Junction_Core_Exception
	 * @return String
	 */
	public function translate($name) {
		foreach ($this as $property) {
			if ($property->getName() == $name) {
				return $property->getColumn();
			}
		}
		throw new Junction_Core_Exception("No column is associated with the name: " . $name);
	}
	
	/**
	 * Push a database result into a new client object.
	 * 
	 * <p>This assumes that the given array is an associative array
	 * whose keys are the column names as defined in the metadata.
	 * 
	 * @param array $result db result set
	 * @return Object client object as defined in the schema
	 */
	public function push(array $result) {
		$client = Junction_Utils_Reflection_Facade::getInstance($this->_classname, array());
		foreach ($this as $property) {
			$this->setProperty($client, $property, $result[$property->getColumn()]);
		}
		return $client;
	}
	
	/**
	 * Build the query using only the mapping's data.
	 * 
	 * @param Junction_Query_Builder $builder
	 * @return String query
	 */
	private function build(Junction_Query_Builder $builder) {
		$builder->bindTable($this->_table);
		$builder->bindKey($this->_id->getColumn(), null);
		foreach ($this->_properties as $property) {
			$builder->bindProperty($property->getColumn(), null);
		}
		return $builder->toSql();
	}
	
	/**
	 * Build the query using the mapping's data and the object's state.
	 * 
	 * @param Junction_Query_Builder $builder
	 * @return String query
	 */
	private function buildWithObject(Junction_Query_Builder $builder, $object) {
		$builder->bindTable($this->_table);
		$builder->bindKey($this->_id->getColumn(), $this->getProperty($object, $this->_id));
		foreach ($this->_properties as $property) {
			$builder->bindProperty($property->getColumn(), $this->getProperty($object, $property));
		}
		return $builder->toSql();
	}
	
	/**
	 * Retrieve the value of the property as represented in the client object.
	 * 
	 * @param Object $object
	 * @param Junction_Core_Property $property
	 * @return unknown
	 */
	public function getProperty($object, Junction_Core_Property $property) {
		return Junction_Utils_Reflection_Facade::invokeArgs($object, MAPPING_GET . $property->getName(), array());
	}
	
	/**
	 * Call the setter method of the object which corresponds to the given property.
	 * 
	 * @param Object $object
	 * @param Junction_Core_Property $property
	 * @param unknown $value
	 * @return unknown
	 */
	public function setProperty($object, Junction_Core_Property $property, $value) {
		return Junction_Utils_Reflection_Facade::invokeArgs($object, MAPPING_SET . $property->getName(), array($value));
	}
}
?>